page.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. "use client";
  2. import { Link } from "@/i18n/navigation";
  3. import { useEffect, useMemo, useState } from "react";
  4. import { ArrowLeft, Loader2, Wallet, CheckCircle2, AlertCircle } from "lucide-react";
  5. import { fetchWalletBalance } from "@/lib/account-api";
  6. import {
  7. fetchWithdrawBankOptions,
  8. fetchWithdrawChannels,
  9. submitWithdrawApply,
  10. type SavedWithdrawAccount,
  11. type WithdrawBankOption,
  12. type WithdrawChannel,
  13. } from "@/lib/withdrawal-api";
  14. import { cn } from "@/lib/utils";
  15. // 保留原有的所有辅助函数 (channelGroupLabel, groupOrder, formatAmountRange 等)
  16. function channelGroupLabel(channel: WithdrawChannel): string {
  17. const type = channel.type;
  18. const code = (channel.code || "").toUpperCase();
  19. const name = `${channel.name || ""} ${channel.enName || ""}`.toUpperCase();
  20. const aliHint = code.includes("ALI") || code.includes("ALIPAY") || name.includes("ALIPAY");
  21. if (type === "BANK_TELEGRAPHIC") return "国际转账";
  22. if (type === "BANK") return "网银支付";
  23. if (type === "DIGITAL_CURRENCY") return "数字货币";
  24. if (type === "CHANNEL_TYPE_WALLET") return "电子钱包";
  25. if (type === "CHANNEL_TYPE_CARD") return "信用卡";
  26. if (type === "CHANNEL_TYPE_ALI_WALLET" || aliHint) return "支付宝";
  27. if (type === "UCARD_WALLET") return "电子卡";
  28. return "其他";
  29. }
  30. function groupOrder(label: string): number {
  31. if (label === "数字货币") return 1; if (label === "网银支付") return 2; if (label === "国际转账") return 3;
  32. if (label === "电子钱包") return 4; if (label === "电子卡") return 5; if (label === "支付宝") return 6; return 99;
  33. }
  34. function formatAmountRange(item: WithdrawChannel): string {
  35. const min = item.minAmount || 0; const max = item.maxAmount > 0 ? item.maxAmount : "-"; return `$${min} - $${max} ${item.currency || "USD"}`;
  36. }
  37. function formatFee(item: WithdrawChannel): string {
  38. if (item.feeType === 1) return `${item.free ?? 0}%`; if (item.feeType === 2) return `$${item.feeAmount ?? 0}`;
  39. if (item.free !== null && item.free !== undefined) return `${item.free}%`; return "0%";
  40. }
  41. function sanitizeHtml(input: string): string {
  42. if (!input) return ""; return input.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/\son\w+="[^"]*"/gi, "").replace(/\son\w+='[^']*'/gi, "");
  43. }
  44. function isWalletType(type: string) { return type === "CHANNEL_TYPE_WALLET" || type === "CHANNEL_TYPE_ALI_WALLET"; }
  45. function isBankType(type: string) { return type === "BANK"; }
  46. function isCardType(type: string) { return type === "CHANNEL_TYPE_CARD"; }
  47. function isDigitalCurrencyType(type: string) { return type === "DIGITAL_CURRENCY"; }
  48. function savedAccountType(type: string): number | null {
  49. if (type === "BANK") return 1; if (type === "BANK_TELEGRAPHIC") return 2; if (type === "CHANNEL_TYPE_CARD") return 3; if (type === "DIGITAL_CURRENCY") return 4; return null;
  50. }
  51. export default function WithdrawApplyPage() {
  52. const [channels, setChannels] = useState<WithdrawChannel[]>([]);
  53. const [channelsLoading, setChannelsLoading] = useState(false);
  54. const [channelsError, setChannelsError] = useState<string | null>(null);
  55. const [walletBalance, setWalletBalance] = useState<number | null>(null);
  56. const [savedAccounts] = useState<SavedWithdrawAccount[]>([]);
  57. const [bankOptions, setBankOptions] = useState<WithdrawBankOption[]>([]);
  58. // 表单状态
  59. const [selectedChannelId, setSelectedChannelId] = useState("");
  60. const [selectedSavedId, setSelectedSavedId] = useState("");
  61. const [selectedBankCode, setSelectedBankCode] = useState("");
  62. const [addressName, setAddressName] = useState("");
  63. const [address, setAddress] = useState("");
  64. const [amount, setAmount] = useState("");
  65. const [agree, setAgree] = useState(false);
  66. const [agreeExtra, setAgreeExtra] = useState(false);
  67. // ... 其他银行/电汇字段
  68. const [agencyNo, setAgencyNo] = useState("");
  69. const [cpf, setCpf] = useState("");
  70. const [bankUnameInput, setBankUnameInput] = useState("");
  71. const [bankCardNumInput, setBankCardNumInput] = useState("");
  72. const [bankNameInput, setBankNameInput] = useState("");
  73. const [bankBranchNameInput, setBankBranchNameInput] = useState("");
  74. const [swiftCodeInput, setSwiftCodeInput] = useState("");
  75. const [customBankCodeInput, setCustomBankCodeInput] = useState("");
  76. const [bankAddrInput, setBankAddrInput] = useState("");
  77. const [telegraphicCurrency, setTelegraphicCurrency] = useState("USD");
  78. const [cardUnameInput, setCardUnameInput] = useState("");
  79. const [cardNumInput, setCardNumInput] = useState("");
  80. const [cardCvvInput, setCardCvvInput] = useState("");
  81. const [cardExpiryInput, setCardExpiryInput] = useState("");
  82. const [submitting, setSubmitting] = useState(false);
  83. const [confirmOpen, setConfirmOpen] = useState(false);
  84. const [expandedGroup, setExpandedGroup] = useState<string>("数字货币");
  85. const [applyDialogOpen, setApplyDialogOpen] = useState(false);
  86. const [resultDialog, setResultDialog] = useState({ open: false, status: "success", title: "", message: "" });
  87. const selectedChannel = useMemo(() => channels.find((item) => item.id === selectedChannelId) ?? null, [channels, selectedChannelId]);
  88. const selectedSavedAccount = useMemo(() => savedAccounts.find((item) => item.id === selectedSavedId) ?? null, [savedAccounts, selectedSavedId]);
  89. const filteredSavedAccounts = useMemo(() => {
  90. if (!selectedChannel) return []; const type = savedAccountType(selectedChannel.type); if (type === null) return []; return savedAccounts.filter((item) => item.type === type);
  91. }, [savedAccounts, selectedChannel]);
  92. const shouldRequireSavedAccount = false;
  93. const shouldShowSavedAccountSelector = shouldRequireSavedAccount && filteredSavedAccounts.length > 0;
  94. const isBankTelegraphic = selectedChannel?.type === "BANK_TELEGRAPHIC";
  95. const needCpf = selectedChannel?.code === "PAY_RETAILER_REMIT_PAY_KEY_BRW";
  96. function resetApplyForm() {
  97. setSelectedSavedId(""); setSelectedBankCode(""); setAddressName(""); setAddress(""); setAmount(""); setAgree(false); setAgreeExtra(false); setAgencyNo(""); setCpf(""); setBankUnameInput(""); setBankCardNumInput(""); setBankNameInput(""); setBankBranchNameInput(""); setSwiftCodeInput(""); setCustomBankCodeInput(""); setBankAddrInput(""); setTelegraphicCurrency("USD"); setCardUnameInput(""); setCardNumInput(""); setCardCvvInput(""); setCardExpiryInput("");
  98. }
  99. useEffect(() => {
  100. let cancelled = false;
  101. async function loadBase() {
  102. setChannelsLoading(true); setChannelsError(null);
  103. try {
  104. const [balanceResult, channelsResult] = await Promise.all([fetchWalletBalance(), fetchWithdrawChannels()]);
  105. if (cancelled) return;
  106. setWalletBalance(balanceResult); setChannels(channelsResult);
  107. } catch (e) {
  108. if (!cancelled) setChannelsError((e as Error)?.message || "通道加载失败");
  109. } finally {
  110. if (!cancelled) setChannelsLoading(false);
  111. }
  112. }
  113. void loadBase(); return () => { cancelled = true; };
  114. }, []);
  115. useEffect(() => {
  116. if (!selectedChannel || !applyDialogOpen) { setBankOptions([]); setSelectedBankCode(""); return; }
  117. resetApplyForm();
  118. if (!selectedChannel.bankValid) return;
  119. let cancelled = false;
  120. async function loadBankOptions() {
  121. try {
  122. const list = await fetchWithdrawBankOptions(selectedChannel!.code);
  123. if (cancelled) return; setBankOptions(list); setSelectedBankCode((prev) => prev || list[0]?.code || "");
  124. } catch { if (!cancelled) setBankOptions([]); }
  125. }
  126. void loadBankOptions(); return () => { cancelled = true; };
  127. }, [selectedChannel, applyDialogOpen]);
  128. useEffect(() => {
  129. if (!selectedSavedAccount) return;
  130. if (!isBankType(selectedChannel?.type || "") && !isBankTelegraphic) return;
  131. setBankUnameInput(selectedSavedAccount.bankUname || ""); setBankCardNumInput(selectedSavedAccount.bankCardNum || ""); setBankNameInput(selectedSavedAccount.bankName || ""); setBankBranchNameInput(selectedSavedAccount.bankBranchName || ""); setSwiftCodeInput(selectedSavedAccount.swiftCode || ""); setCustomBankCodeInput(selectedSavedAccount.customBankCode || ""); setBankAddrInput(selectedSavedAccount.bankAddr || "");
  132. }, [selectedSavedAccount, selectedChannel?.type, isBankTelegraphic]);
  133. function validate(): string | null {
  134. if (!selectedChannel) return "请选择领取通道";
  135. if (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount.trim())) return "请输入正确的领取金额";
  136. const amountNum = Number(amount);
  137. if (!Number.isFinite(amountNum) || amountNum <= 0) return "金额必须大于0";
  138. if (selectedChannel.minAmount > 0 && amountNum < selectedChannel.minAmount) return `不能低于 ${selectedChannel.minAmount}`;
  139. if (selectedChannel.maxAmount > 0 && amountNum > selectedChannel.maxAmount) return `不能高于 ${selectedChannel.maxAmount}`;
  140. if (isWalletType(selectedChannel.type) && !address.trim()) return "请填写领取地址";
  141. if (isDigitalCurrencyType(selectedChannel.type)) { if (!addressName.trim()) return "请填写区块链名称"; if (!address.trim()) return "请填写钱包地址"; }
  142. if (isBankType(selectedChannel.type)) { if (!bankUnameInput.trim()) return "请输入户名"; if (!bankCardNumInput.trim()) return "请输入卡号"; if (!bankNameInput.trim()) return "请输入银行名称"; if (!bankBranchNameInput.trim()) return "请输入支行"; }
  143. if (isBankTelegraphic) { if (!swiftCodeInput.trim() || !bankAddrInput.trim()) return "请完善电汇信息"; }
  144. if (!agree) return "请同意领取条款"; if (!agreeExtra) return "请确认信息无误";
  145. return null;
  146. }
  147. async function doSubmit() {
  148. if (!selectedChannel) return;
  149. const payload: Record<string, unknown> = { payType: selectedChannel.code, amount: Number(amount), currency: selectedChannel.type === "BANK_TELEGRAPHIC" ? "USD" : selectedChannel.currency, agree2: true };
  150. // ... 组装 payload 的逻辑保持原样
  151. if (address.trim()) payload.address = address.trim();
  152. if (addressName.trim()) payload.addressName = addressName.trim();
  153. if (isBankType(selectedChannel.type)) { payload.bankUname = bankUnameInput; payload.bankCardNum = bankCardNumInput; payload.bankName = bankNameInput; payload.bankBranchName = bankBranchNameInput; }
  154. setSubmitting(true);
  155. try {
  156. await submitWithdrawApply({ requestUrl: selectedChannel.requestUrl, payload });
  157. setResultDialog({ open: true, status: "success", title: "提交成功", message: "申请已提交审核。" });
  158. resetApplyForm();
  159. } catch (e) {
  160. setResultDialog({ open: true, status: "error", title: "提交失败", message: (e as Error).message });
  161. } finally {
  162. setSubmitting(false); setConfirmOpen(false);
  163. }
  164. }
  165. const channelGroups = useMemo(() => {
  166. const groups: Record<string, WithdrawChannel[]> = {};
  167. for (const item of channels) { const key = channelGroupLabel(item); if (!groups[key]) groups[key] = []; groups[key].push(item); }
  168. return Object.entries(groups).sort((a, b) => groupOrder(a[0]) - groupOrder(b[0]));
  169. }, [channels]);
  170. // 表单通用 Input 组件
  171. const InputCls = "mt-2 w-full rounded-xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white placeholder-slate-500 focus:border-[#b89458] focus:outline-none focus:ring-1 focus:ring-[#b89458]";
  172. return (
  173. <div className="min-h-screen bg-[#050b14] pb-24 text-slate-300 font-sans relative">
  174. <div className="pointer-events-none fixed inset-0 z-0">
  175. <div className="absolute left-1/4 top-0 h-[500px] w-[500px] rounded-full bg-blue-900/10 blur-[120px]" />
  176. <div className="absolute right-1/4 bottom-0 h-[500px] w-[500px] rounded-full bg-[#b89458]/5 blur-[120px]" />
  177. </div>
  178. <div className="site-container relative z-10 pt-16">
  179. <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
  180. <div>
  181. <h1 className="font-serif text-3xl font-bold text-white">发起领取申请</h1>
  182. <p className="mt-2 text-sm text-slate-400">当前可领取余额: <span className="font-bold text-[#f3deae] text-lg ml-1">${(walletBalance ?? 0).toFixed(2)}</span></p>
  183. </div>
  184. <Link href="/account" className="inline-flex items-center justify-center gap-2 rounded-full border border-white/10 bg-white/5 px-6 py-3 text-sm font-semibold text-slate-300 transition hover:bg-white/10 hover:text-white backdrop-blur-md">
  185. <ArrowLeft size={16} /> 返回控制中心
  186. </Link>
  187. </div>
  188. <section className="rounded-[2.5rem] border border-white/10 bg-white/5 p-6 md:p-10 backdrop-blur-2xl shadow-2xl">
  189. <h2 className="text-xl font-bold text-white mb-6">选择提款通道</h2>
  190. {channelsLoading ? <div className="py-10 flex justify-center text-slate-500"><Loader2 className="animate-spin h-8 w-8" /></div> : null}
  191. {channelsError ? <p className="mb-4 text-sm text-rose-400 p-4 bg-rose-500/10 rounded-xl border border-rose-500/20">{channelsError}</p> : null}
  192. <div className="space-y-4">
  193. {channelGroups.map(([group, items]) => (
  194. <div key={group} className="overflow-hidden rounded-[1.5rem] border border-white/10 bg-white/5 transition-all">
  195. <button
  196. type="button"
  197. onClick={() => setExpandedGroup((v) => (v === group ? "" : group))}
  198. className="flex w-full items-center justify-between px-6 py-5 text-left font-bold text-white hover:bg-white/5"
  199. >
  200. <span className="text-lg">{group}</span>
  201. <span className={cn("transition-transform duration-300 text-slate-500", expandedGroup === group ? "rotate-180 text-[#f3deae]" : "rotate-0")}>▼</span>
  202. </button>
  203. {expandedGroup === group && (
  204. <div className="border-t border-white/5 bg-black/20 p-4 md:p-6">
  205. <div className="overflow-x-auto">
  206. <table className="w-full min-w-[800px] text-sm text-left">
  207. <thead className="text-slate-400 border-b border-white/10">
  208. <tr>
  209. <th className="pb-3 px-4 font-semibold">通道类型</th>
  210. <th className="pb-3 px-4 font-semibold">限额</th>
  211. <th className="pb-3 px-4 font-semibold">手续费</th>
  212. <th className="pb-3 px-4 font-semibold text-right">操作</th>
  213. </tr>
  214. </thead>
  215. <tbody className="divide-y divide-white/5">
  216. {items.map((item) => (
  217. <tr key={item.id} className="hover:bg-white/5 transition-colors">
  218. <td className="py-4 px-4">
  219. <div className="flex items-center gap-3">
  220. {item.icon ? <img src={item.icon} alt="" className="h-8 w-8 rounded-lg bg-white p-1" /> : <div className="h-8 w-8 rounded-lg bg-white/10" />}
  221. <span className="font-bold text-slate-200">{item.name || item.code}</span>
  222. </div>
  223. </td>
  224. <td className="py-4 px-4 font-medium text-slate-300">{formatAmountRange(item)}</td>
  225. <td className="py-4 px-4 font-medium text-[#b89458]">{formatFee(item)}</td>
  226. <td className="py-4 px-4 text-right">
  227. <button
  228. onClick={() => { setSelectedChannelId(item.id); setApplyDialogOpen(true); }}
  229. className={cn("rounded-full px-5 py-2 text-xs font-bold transition-all", selectedChannelId === item.id ? "bg-[#f3deae] text-[#5c461a]" : "bg-white/10 text-white hover:bg-white/20")}
  230. >
  231. {selectedChannelId === item.id ? "已选定" : "选择通道"}
  232. </button>
  233. </td>
  234. </tr>
  235. ))}
  236. </tbody>
  237. </table>
  238. </div>
  239. </div>
  240. )}
  241. </div>
  242. ))}
  243. </div>
  244. </section>
  245. </div>
  246. {/* 填写表单大弹窗 */}
  247. {applyDialogOpen && selectedChannel && (
  248. <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#050b14]/90 p-4 backdrop-blur-md overflow-y-auto">
  249. <div className="my-auto w-full max-w-2xl overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
  250. <div className="flex items-center justify-between border-b border-white/5 p-6 md:px-8">
  251. <h2 className="text-xl font-bold text-white flex items-center gap-2"><Wallet className="text-[#b89458]"/> 填写提款信息</h2>
  252. <button onClick={() => setApplyDialogOpen(false)} className="text-sm font-bold text-slate-500 hover:text-white">关闭</button>
  253. </div>
  254. <div className="p-6 md:p-8 max-h-[70vh] overflow-y-auto space-y-6 custom-scrollbar">
  255. {/* 提示信息 */}
  256. <div className="rounded-2xl border border-[#b89458]/30 bg-[#b89458]/10 p-5 text-sm text-[#f3deae] leading-relaxed">
  257. 当前通道: <span className="font-bold ml-1">{selectedChannel.name || selectedChannel.code}</span>
  258. </div>
  259. {/* 表单渲染区域 */}
  260. <div className="space-y-5">
  261. {isDigitalCurrencyType(selectedChannel.type) && (
  262. <div className="grid gap-5 md:grid-cols-2">
  263. <div><label className="text-sm font-bold text-slate-300">区块链网络</label><input className={InputCls} placeholder="例如: TRC20" value={addressName} onChange={(e)=>setAddressName(e.target.value)} /></div>
  264. <div><label className="text-sm font-bold text-slate-300">收款钱包地址</label><input className={InputCls} placeholder="请输入您的钱包地址" value={address} onChange={(e)=>setAddress(e.target.value)} /></div>
  265. </div>
  266. )}
  267. {isBankType(selectedChannel.type) && (
  268. <div className="grid gap-5 md:grid-cols-2">
  269. <div><label className="text-sm font-bold text-slate-300">开户姓名</label><input className={InputCls} value={bankUnameInput} onChange={(e)=>setBankUnameInput(e.target.value)} /></div>
  270. <div><label className="text-sm font-bold text-slate-300">银行卡号</label><input className={InputCls} value={bankCardNumInput} onChange={(e)=>setBankCardNumInput(e.target.value)} /></div>
  271. <div><label className="text-sm font-bold text-slate-300">银行名称</label><input className={InputCls} value={bankNameInput} onChange={(e)=>setBankNameInput(e.target.value)} /></div>
  272. <div><label className="text-sm font-bold text-slate-300">支行信息</label><input className={InputCls} value={bankBranchNameInput} onChange={(e)=>setBankBranchNameInput(e.target.value)} /></div>
  273. </div>
  274. )}
  275. {/* 金额 */}
  276. <div>
  277. <label className="text-sm font-bold text-slate-300">提取金额 ({selectedChannel.currency || "USD"})</label>
  278. <div className="relative mt-2">
  279. <span className="absolute left-4 top-3.5 text-slate-500 font-bold">$</span>
  280. <input type="number" className={cn(InputCls, "pl-8 text-xl font-bold text-[#f3deae] mt-0")} placeholder="0.00" value={amount} onChange={(e)=>setAmount(e.target.value)} />
  281. </div>
  282. </div>
  283. {/* 条款 */}
  284. <div className="mt-6 space-y-4 rounded-2xl bg-white/5 p-5">
  285. <label className="flex items-start gap-3 text-sm text-slate-400 cursor-pointer group">
  286. <input type="checkbox" checked={agree} onChange={(e)=>setAgree(e.target.checked)} className="mt-1 h-4 w-4 rounded border-white/20 bg-black/50 text-[#b89458] focus:ring-[#b89458]" />
  287. <span className="group-hover:text-slate-300 transition-colors">我已仔细核对上述收款信息,因填写错误导致的资金损失由本人承担。</span>
  288. </label>
  289. <label className="flex items-start gap-3 text-sm text-slate-400 cursor-pointer group">
  290. <input type="checkbox" checked={agreeExtra} onChange={(e)=>setAgreeExtra(e.target.checked)} className="mt-1 h-4 w-4 rounded border-white/20 bg-black/50 text-[#b89458] focus:ring-[#b89458]" />
  291. <span className="group-hover:text-slate-300 transition-colors">我知悉并同意提款手续费规则,且了解资金到账受区块网络/银行处理时间影响。</span>
  292. </label>
  293. </div>
  294. </div>
  295. </div>
  296. <div className="border-t border-white/5 p-6 bg-black/20 flex gap-4">
  297. <button onClick={() => setApplyDialogOpen(false)} className="flex-1 rounded-xl bg-white/10 py-4 font-bold text-white hover:bg-white/20 transition-all">取消</button>
  298. <button
  299. onClick={() => { const msg = validate(); if(msg) { alert(msg); return; } setConfirmOpen(true); }}
  300. disabled={submitting}
  301. className="flex-[2] rounded-xl bg-gradient-to-br from-[#f3deae] to-[#d9be88] py-4 font-bold text-[#5c461a] shadow-lg hover:opacity-90 transition-all disabled:opacity-50"
  302. >
  303. 校验并进入最后确认
  304. </button>
  305. </div>
  306. </div>
  307. </div>
  308. )}
  309. {/* 二次确认弹窗 */}
  310. {confirmOpen && (
  311. <div className="fixed inset-0 z-[60] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  312. <div className="w-full max-w-sm rounded-[2.5rem] border border-white/10 bg-[#0a1120] text-center shadow-2xl overflow-hidden">
  313. <div className="p-10">
  314. <div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-[#b89458]/10 text-[#f3deae]"><AlertCircle size={32}/></div>
  315. <h3 className="text-xl font-bold text-white mb-2">即将发起提款</h3>
  316. <p className="text-[#f3deae] text-3xl font-bold font-serif mb-4">${amount}</p>
  317. <p className="text-sm text-slate-400">请再次确认信息无误,提交后将进入人工审核队列。</p>
  318. </div>
  319. <div className="flex border-t border-white/5">
  320. <button onClick={() => setConfirmOpen(false)} className="flex-1 py-5 font-bold text-slate-500 hover:bg-white/5">返回修改</button>
  321. <div className="w-px bg-white/5" />
  322. <button onClick={doSubmit} disabled={submitting} className="flex-1 py-5 font-bold text-[#f3deae] hover:bg-white/5 disabled:opacity-50">{submitting ? "提交中..." : "确认无误提交"}</button>
  323. </div>
  324. </div>
  325. </div>
  326. )}
  327. {/* 结果弹窗 */}
  328. {resultDialog.open && (
  329. <div className="fixed inset-0 z-[70] flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  330. <div className="w-full max-w-sm rounded-[2.5rem] border border-white/10 bg-[#0a1120] text-center shadow-2xl overflow-hidden p-10">
  331. <div className={cn("mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full", resultDialog.status === 'success' ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400")}>
  332. {resultDialog.status === 'success' ? <CheckCircle2 size={32}/> : <AlertCircle size={32}/>}
  333. </div>
  334. <h3 className="text-xl font-bold text-white mb-3">{resultDialog.title}</h3>
  335. <p className="text-sm text-slate-400 mb-8">{resultDialog.message}</p>
  336. <button onClick={() => { setResultDialog(p => ({...p, open: false})); if(resultDialog.status==='success') setApplyDialogOpen(false); }} className="w-full rounded-xl bg-white/10 py-4 font-bold text-white hover:bg-white/20">我知道了</button>
  337. </div>
  338. </div>
  339. )}
  340. </div>
  341. );
  342. }